"""Module for common tools dealing with Geometry."""
from __future__ import annotations

import json
import uuid
import random
from typing import Optional, Dict, Union, Any, List, Tuple

import numpy as np
from pyproj import Geod
import geopandas as gpd
import folium
from shapely import wkt, wkb
from shapely.ops import split, unary_union
from shapely.geometry import shape, LineString, Point, Polygon
from shapely.geometry.base import BaseGeometry
from osgeo import ogr

from common.logger import logger
from common.typing_utils import numeric
from common.geometry.bbox import BoundingBox

ogr.UseExceptions()

GEOD_WGS84 = Geod(ellps="WGS84")


class Geom(object):
    def __init__(self, geointerface_like: Union[BaseGeometry, Dict, Any]):
        """Initialize a Geom by a given object that is compatible with the `geointerface` protocol.

        Parameters
        ----------
        geointerface_like : Union[BaseGeometry, Dict, Any]
            An object that is compatible with the [`geointerface`](https://gist.github.com/sgillies/2217756) protocol.

        Raises
        ------
        TypeError
            Raised if the given object is not compatible with the `geointerface` protocol.

        Note
        ----
            If the given `geointerface_like` object is a `shapely.geometry.BaseGeometry`,
            the created `Geom` object is a simple wrapper of the given object.
            Otherwise, a copy of the given object will be created and held by the created `Geom` object.
        """
        if isinstance(geointerface_like, BaseGeometry):
            self._shapely_geom = geointerface_like
        elif isinstance(geointerface_like, Dict) and (
            "__geo_interface__" in geointerface_like
        ):
            self._shapely_geom = shape(geointerface_like["__geo_interface__"])
        else:
            try:
                self._shapely_geom = shape(geointerface_like)
            except Exception:
                raise TypeError(
                    "Only accepting a geometry supporting the `__geo_interface__`"
                )

    @property
    def __geo_interface__(self) -> Dict:
        """Exposing the __geo_interface__ protocol."""
        return self._shapely_geom.__geo_interface__

    @staticmethod
    def create_an_empty_geom(geo_type="Polygon") -> Geom:
        """Create an empty geometry with given type.

        Parameters
        ----------
        geo_type : str, optional
            Type of the geometry, could be one of "Point", "MultiPoint",
            "LineString", "MultiLineString", "Polygon", "MultiPolygon",
            by default "Polygon".

        Returns
        -------
        Geom
            The created empty geometry.

        Raises
        ------
        ValueError
            Raised when the given geometry type is not supported.
        """
        geo_type = geo_type.upper()
        if geo_type not in (
            "POINT",
            "MULTIPOINT",
            "LINESTRING",
            "MULTILINESTRING",
            "POLYGON",
            "MULTIPOLYGON",
        ):
            raise ValueError(f"`geo_type` {geo_type} is not a supported one.")
        else:
            return Geom.from_wkt(f"{geo_type} EMPTY")

    @staticmethod
    def from_ogr(ogr_geom: ogr.Geometry) -> Geom:
        """Create a `Geom` object from a given `ogr.Geometry` object"""
        wkt_str = ogr_geom.ExportToWkt()
        return Geom.from_wkt(wkt_str)

    @staticmethod
    def from_shapely(shapely_geom: BaseGeometry, copied: bool = False) -> Geom:
        """Create a `Geom` object from a give `shapely.geometry.BaseGeometry` object

        Parameters
        ----------
        shapely_geom : BaseGeometry
            A given `shapely.geometry.BaseGeometry` object.
        copied : bool, optional
            Whether to create a copy of the given object, by default False.

        Returns
        -------
        Geom
            Created `Geom`
        """
        if copied:
            return Geom.from_wkt(shapely_geom.wkt)
        else:
            return Geom(shapely_geom)

    @staticmethod
    def from_geometry_json(geometry_json: str) -> Geom:
        """Create a `Geom` object from a give string as a json string representing a geometry.

        Parameters
        ----------
        geometry_json : str
            The given geometry json string, following the geometry's part in the geojson's specification,
            as in <https://datatracker.ietf.org/doc/html/rfc7946>.

        Returns
        -------
        Geom
            Created `Geom`
        """
        return Geom(json.loads(geometry_json))

    @staticmethod
    def from_geojson_dict(geojson_dict: dict, select: str = "union") -> Geom:
        """Create a `Geom` object from a give geojson dictionary.

        Parameters
        ----------
        geojson_dict: dict
            The given geojson dictionary, following the geometry's part in the geojson's specification,
            as in <https://datatracker.ietf.org/doc/html/rfc7946>.
        select: str
            What to do when multiple Feature/Geometry presents in the given geojson string,
            could be one of "first" (use the geometry of the first feature), "last" (use the last one),
            "random" (randomly choose one), or "union" (use the geometry created by a unary union of all geometries),
            by default "union".

        Returns
        -------
        Geom
            Created `Geom`

        Raises
        ------
        ValueError
            Raised when the given `geojson_str` is not a valid one with geometry to be detected.
            Raised when the given `select` method is not a supported one.
        """
        all = geojson_dict
        if "features" not in all:
            logger.warning(
                "The given `geojson_str` is not a standard one, try to guess now."
            )
            if "geometry" in all:
                logger.warning("Detect the `geometry` field.")
                geom_dict = all["geometry"]
                return Geom(geom_dict)
            else:
                raise ValueError(
                    "The given `geojson_str` cannot be identified as a valid str with geometry."
                )

        if len(all["features"]) == 1:
            geom_dict = all["features"][0]["geometry"]
        else:
            if select == "first":
                ind = 0
            elif select == "last":
                ind = -1
            elif select == "random":
                total = len(all["features"])
                if total == 1:
                    ind = 0
                else:
                    ind = random.randint(0, total - 1)
            elif select == "union":
                geoms = [shape(item["geometry"]) for item in all["features"]]
                union_geom = unary_union(geoms)
                return Geom(union_geom)
            else:
                raise ValueError("Unknown `select`.")
            geom_dict = all["features"][ind]["geometry"]
        return Geom(geom_dict)

    @staticmethod
    def from_geojson(geojson_str: str, select: str = "union") -> Geom:
        """Create a `Geom` object from a give geojson string.

        Parameters
        ----------
        geojson_str: str
            The given geojson string, following the geometry's part in the geojson's specification,
            as in <https://datatracker.ietf.org/doc/html/rfc7946>.
        select: str
            What to do when multiple Feature/Geometry presents in the given geojson string,
            could be one of "first" (use the geometry of the first feature), "last" (use the last one),
            "random" (randomly choose one), or "union" (use the geometry created by a unary union of all geometries),
            by default "union".

        Returns
        -------
        Geom
            Created `Geom`

        Raises
        ------
        ValueError
            Raised when the given `geojson_str` is not a valid one with geometry to be detected.
            Raised when the given `select` method is not a supported one.
        """
        all = json.loads(geojson_str)
        return Geom.from_geojson_dict(all, select=select)

    @staticmethod
    def from_geojson_fpath(fpath_geojson: str, select: str = "union") -> Geom:
        """Create a `Geom` object from a give geojson string.

        Parameters
        ----------
        fpath_geojson: str
            Path to a geojson file.
        select: str
            What to do when multiple Feature/Geometry presents in the given geojson string,
            see `Geom.from_geojson` for details, by default "union".

        Returns
        -------
        Geom
            Created `Geom`
        """
        with open(fpath_geojson) as fin:
            geom = Geom.from_geojson(fin.read(), select=select)
        return geom

    @staticmethod
    def from_wkt(wkt_str: str) -> Geom:
        """Create a `Geom` object from a given WKT (Well-Known-Text) string"""
        return Geom(wkt.loads(wkt_str))

    @staticmethod
    def from_wkb(wkb_bytes: bytes) -> Geom:
        """Create a `Geom` object from a given WKB (Well-Known-Binary) bytes object"""
        return Geom(wkb.loads(wkb_bytes))

    @staticmethod
    def from_bbox(bbox: BoundingBox) -> Geom:
        """Create a polygon `Geom` object from a given `BoundingBox` object."""
        return bbox_to_geom(bbox)

    @staticmethod
    def make_point(
        coord: Union[Tuple[numeric, numeric], Tuple[numeric, numeric, numeric]]
    ) -> Geom:
        """Make a `Point` geometry object

        Parameters
        ----------
        coord : Union[Tuple[numeric, numeric], Tuple[numeric, numeric, numeric]]
            coordinate of the point, either 2 elements of (x, y), or 3 elements of (x, y, z)

        Returns
        -------
        Geom
            The created `Point` geometry
        """
        return Geom(Point(coord))

    @staticmethod
    def make_linestring(
        coords: List[Union[Tuple[numeric, numeric], Tuple[numeric, numeric, numeric]]]
    ) -> Geom:
        """Make a `LineString` geometry object.

        Parameters
        ----------
        coords : List[Union[Tuple[numeric, numeric], Tuple[numeric, numeric]]]
            coordinates for the linestring to be created, should be a list of coordinates,
            with each coordinate a tuple of either 2 elements (x, y) or 3 elements (x, y, z).

        Returns
        -------
        Geom
            The `LineString` geometry created.
        """
        return Geom(LineString(coords))

    @staticmethod
    def make_polygon(
        shell_coords: List[
            Union[Tuple[numeric, numeric], Tuple[numeric, numeric, numeric]]
        ]
    ) -> Geom:
        """Make a `Polygon` geometry object.

        Parameters
        ----------
        shell_coords : List[Union[Tuple[numeric, numeric], Tuple[numeric, numeric]]]
            Coordinates for the external part of the polygon to be created,
            should be a list of coordinates, with each coordinate a tuple of
            either 2 elements (x, y) or 3 elements (x, y, z).
            Notice There is no need to add a last coordinate replicating
            the first coordinate to finish the line as a ring.

        Returns
        -------
        Geom
            The `Polygon` geometry created.
        """
        return Geom(Polygon(shell=shell_coords))

    def to_ogr(self) -> ogr.Geometry:
        """Convert to a `ogr.Geometry` object."""
        return ogr.CreateGeometryFromWkt(self.to_wkt())

    def to_ogr_mem_ds(self) -> str:
        """Convert to a string pointer to the in-memory `ogr.DataSource`

        Notes
        -----
        Must release the memory after using the returned `ogr.DataSource`
        Examples:
        >>> ds_name = geom.to_ogr_mem_ds()
        >>> {code for using `ds_name`}
        >>> gdal.Unlink(ds_name)
        """
        ds_name = f"/vsimem/{uuid.uuid4()}.json"
        ds = ogr.GetDriverByName("GeoJSON").CreateDataSource(ds_name)
        lyr = ds.CreateLayer("geometry")
        f = ogr.Feature(lyr.GetLayerDefn())
        f.SetGeometry(ogr.CreateGeometryFromWkt(self.to_wkt()))
        lyr.CreateFeature(f)
        f = None
        lyr = None
        ds = None
        return ds_name

    def to_shapely(self) -> BaseGeometry:
        """Convert to a `shapely.geometry.Geometry` object."""
        return shape(self.__geo_interface__)

    def to_map(self, zoom=5) -> folium.Map:
        """Create a folium map object to show geom to a map.

        Parameters
        ----------
        zoom: int
            The starting zoom level to show the map.

        Returns
        -------
        folium.Map
            A folium Map object displaying the map.
        """
        g = gpd.GeoSeries(self.to_shapely(), crs="EPSG:4326")
        cx, cy = self.centroid_xy
        m = folium.Map(location=[cy, cx], zoom_start=zoom)
        folium.GeoJson(g.geometry).add_to(m)
        return m

    def to_geometry(self) -> dict:
        """Convert to a geometry dict as specified by geojson specification's geometry part."""
        return self._shapely_geom.__geo_interface__

    def to_geometry_json(self) -> str:
        """Convert to a geometry string as specified by geojson specification's geometry part."""
        return json.dumps(self._shapely_geom.__geo_interface__)

    def to_geojson_dict(self) -> dict:
        """Convert to a geojson dictionary."""
        geojson_dict = {
            "type": "FeatureCollection",
            "features": [
                {
                    "type": "Feature",
                    "geometry": self._shapely_geom.__geo_interface__,
                },
            ],
        }
        return geojson_dict

    def to_geojson(self) -> str:
        """Convert to a geojson string."""
        geojson_dict = self.to_geojson_dict()
        return json.dumps(geojson_dict)

    def to_geojson_fpath(self, fpath_geojson: str):
        """Convert and write to a geojson file."""
        with open(fpath_geojson, "w") as fout:
            fout.write(self.to_geojson())

    def to_wkt(self) -> str:
        """Convert to a WKT string."""
        return self._shapely_geom.wkt

    def to_wkb(self) -> bytes:
        """Convert to a WKB bytes object."""
        return self._shapely_geom.wkb

    # @property
    # def bounds(self) -> BoundingBox:
    #     return self._shapely_geom.bounds

    @property
    def bounds(self) -> BoundingBox:
        """Bounding box of the geometry"""
        return BoundingBox.from_shapely_bounds(self._shapely_geom.bounds)

    @property
    def area(self) -> float:
        """Area of the geometry, assuming simple planar geometry"""
        return self._shapely_geom.area

    def get_area(self, is_geographical=False) -> float:
        """Get area of the geometry

        Parameters
        ----------
        is_geographical : bool, optional
            Whether the coordinates of the geometry is longitude-latitude, by default False.
            If True, it will calculated the area treating the `Geometry` under geodesic coordinate system,
            and return the area in the unit of $m^2$.

        Returns
        -------
        float
            Area of the geometry.
        """
        if is_geographical:
            area = GEOD_WGS84.geometry_area_perimeter(self._shapely_geom)[0]
            if area < 0:
                # if the orientation of the `Geometry` is not CCW, `area` would be a negative value
                logger.warning(
                    (
                        "Notice the geometry is not CCW, resulting in negative `area`."
                        " The `area` is flipped to be positive."
                    )
                )
                area = abs(area)
            return area
        else:
            return self.area

    @property
    def length(self) -> float:
        """Length of the geometry, assuming simple planar geometry"""
        return self._shapely_geom.length

    def get_length(self, is_geographical=False) -> float:
        """Get length of the geometry

        Parameters
        ----------
        is_geographical : bool, optional
            Whether the coordinates of the geometry is longitude-latitude, by default False.

        Returns
        -------
        float
            Length of the geometry.
        """
        if is_geographical:
            return GEOD_WGS84.geometry_length(self._shapely_geom)
        else:
            return self.length

    @property
    def geom_type(self) -> str:
        """Type of geometry"""
        return self._shapely_geom.geom_type

    @property
    def representative_point(self) -> Geom:
        """A representative point for the geometry, calculated in a fast approximate way."""
        return Geom(self._shapely_geom.representative_point)

    @property
    def centroid(self) -> Geom:
        """Centroid of the geometry."""
        return Geom(self._shapely_geom.centroid)

    @property
    def centroid_xy(self) -> Tuple[float, float]:
        """Centroid of the geometry as a coord (x, y)."""
        x = self._shapely_geom.centroid.x
        y = self._shapely_geom.centroid.y
        return (x, y)

    @property
    def boundary(self) -> Geom:
        """Boundary of the geometry."""
        return Geom(self._shapely_geom.boundary)

    @property
    def convex_hull(self) -> Geom:
        """Convex hull of the geometry"""
        return Geom(self._shapely_geom.convex_hull)

    @property
    def n_coords(self) -> int:
        """number of coordinates in the geometry,
        this could be used as a way to estimate complexity, data storage and transfer requirements for the geometry.
        """
        if self.geom_type == "Point":
            ret = 1
        elif self.geom_type == "LineString" or self.geom_type == "LinearRing":
            ret = len(self._shapely_geom.coords)
        elif self.geom_type == "Polygon":
            ret = 0
            ret += len(self._shapely_geom.exterior.coords)
            for tmp_geom in self._shapely_geom.interiors:
                ret += len(tmp_geom.coords)
        elif self.geom_type == "MultiPoint":
            ret = len(self._shapely_geom.geoms)
        elif self.geom_type == "MultiLineString":
            ret = 0
            for tmp_geom in self._shapely_geom.geoms:
                ret += len(self._shapely_geom.coords)
        elif self.geom_type == "MultiPolygon":
            ret = 0
            for tmp_polygon in self._shapely_geom.geoms:
                ret += len(tmp_polygon.exterior.coords)
                for tmp_geom in tmp_polygon.interiors:
                    ret += len(tmp_geom.coords)
        else:
            raise ValueError(
                f"Does not support counting `n_coords` for geometry type {self.geom_type}."
            )
        return ret

    @property
    def is_empty(self) -> bool:
        """Check if the geometry is empty"""
        return self._shapely_geom.is_empty

    @property
    def is_valid(self) -> bool:
        """Check if the geometry is valid"""
        return self._shapely_geom.is_valid

    @property
    def is_simple(self) -> Optional[bool]:
        """Whether the geometry is simple

        Returns
        -------
        Optional[bool]
            None, if the geometry is not a LineString or LinearRing
            True if the LineString or LinearRing does not cross itself, otherwise False.
        """
        if self._shapely_geom.geom_type in ("LineString", "LinearRing"):
            return self._shapely_geom.is_simple
        else:
            return None

    @property
    def is_ring(self) -> Optional[bool]:
        """Whether the geometry is a ring

        Returns
        -------
        Optional[bool]
            None if the geometry is not a LineString.
            True if the LineString is a ring, otherwise False.
        """
        if self._shapely_geom.geom_type == "LineString":
            return self._shapely_geom.is_ring
        else:
            return None

    @property
    def is_ccw(self) -> Optional[bool]:
        """Whether the geometry is counter-clock-wise (CCW).

        Returns
        -------
        Optional[bool]
            None if the geometry is not a LinearRing, Polygon, or MultiPolygon.
            True if the geometry of the supported types is CCW, otherwise False.
        """
        if self._shapely_geom.geom_type == "LinearRing":
            return self._shapely_geom.is_ccw
        elif self._shapely_geom.geom_type == "Polygon":
            return self._shapely_geom.exterior.is_ccw
        elif self._shapely_geom.geom_type == "MultiPolygon":
            for item in self._shapely_geom.geoms:
                if not item.exterior.is_ccw:
                    return False
            else:
                return True
        else:
            return None

    def distance(self, other: Geom, is_geographical=False) -> float:
        """Distance between the geometry to the `other` geometry

        Parameters
        ----------
        other : Geom
            The other geometry.
        is_geographical : bool, optional
            Whether the coordinates of the geometry is longitude-latitude, by default False.

        Returns
        -------
        float
            Distance between the two geometries.
        """
        if is_geographical:
            raise NotImplementedError("later")
        else:
            return self._shapely_geom.distance(other._shapely_geom)

    def contains(self, other: Geom) -> bool:
        """Whether the geometry contains the `other` geometry."""
        return self._shapely_geom.contains(other._shapely_geom)

    def within(self, other: Geom) -> bool:
        """Whether the geometry is within the `other` geometry."""
        return self._shapely_geom.within(other._shapely_geom)

    def intersects(self, other: Geom) -> bool:
        """Whether the geometry intersects with the `other` geometry."""
        return self._shapely_geom.intersects(other._shapely_geom)

    def disjoint(self, other: Geom) -> bool:
        """Whether the geometry disjoints with the `other` geometry."""
        return self._shapely_geom.disjoint(other._shapely_geom)

    def equals(self, other: Geom) -> bool:
        """Whether the geometry equals with the `other` geometry."""
        return self._shapely_geom.equals(other._shapely_geom)

    def union(self, other: Geom) -> Geom:
        """The union of the geometry and the `other` geometry."""
        return Geom(self._shapely_geom.union(other._shapely_geom))

    def intersection(self, other) -> Geom:
        """The intersection of the geometry and the `other` geometry."""
        return Geom(self._shapely_geom.intersection(other._shapely_geom))

    def symmetric_difference(self, other) -> Geom:
        """The symmetric-difference of the geometry and the `other` geometry,
        or in another word, the area in the geometry but not in the `other` geometry,
        plus the area in the `other` geometry but not in the geometry.
        """
        return Geom(self._shapely_geom.symmetric_difference(other._shapely_geom))

    def difference(self, other) -> Geom:
        """The difference of the geometry and the `other` geometry,
        or in another word, the area in the geometry but not in the `other` geometry.
        """
        return Geom(self._shapely_geom.difference(other._shapely_geom))

    def simplify(self, tolerance: float, preserve_topology: bool = True) -> Geom:
        """Simplify the geometry by given criteria, see `shapely.geometry.simplify` for more details."""
        return Geom(
            self._shapely_geom.simplify(
                tolerance=tolerance, preserve_topology=preserve_topology
            )
        )

    def buffer(self, distance: float, **args) -> Geom:
        """Create a new geometry from the given geometry by buffering it with given criteria,
        see `shapely.geometry.buffer` for more details.
        """
        return Geom(self._shapely_geom.buffer(distance, **args))

    def densify(self, step: float) -> Geom:
        """Densify the geometry by a given step.

        Parameters
        ----------
        step : float
            Step for the densify operation.

        Returns
        -------
        Geom
            Resulting geometry from the operation.
            Lines within the resulting geometry would have coordinates that
            nearby coordinates have distances less than the given step.
        """
        ogr_geom = self.to_ogr()
        densified_geom = ogr_geom.Segmentize(step)
        return Geom.from_ogr(densified_geom)

    def fix_not_ccw(self):
        """Fix geometry if it is not counter-clock-wise (CCW), in place."""
        flag_ccw = self.is_ccw()
        if (flag_ccw is None) or flag_ccw:
            # not valid for ccw check, or already ccw, just skip
            return

        if self._shapely_geom.geom_type == "LinearRing":
            self._shapely_geom.coords = list(self._shapely_geom.coords)[::-1]
        elif self._shapely_geom.geom_type == "Polygon":
            self._shapely_geom.exterior.coords = list(
                self._shapely_geom.exterior.coords
            )[::-1]
        elif self._shapely_geom.geom_type == "MultiPolygon":
            for item in self._shapely_geom.geoms:
                if not item.exterior.is_ccw:
                    item.exterior.coords = list(item.exterior.coords)[::-1]
        else:
            # should not arrive here as other geometry type should fall into the condition of `flag_ccw is None`
            pass

    def fix_self_crossing(self) -> Geom:
        """Fix self crossing, returning a newly created fixed geometry.

        Returns
        -------
        Geom
            A new geometry created with self crossing issue fixed.
        """
        fixed = self._shapely_geom.buffer(0.0)
        return Geom(fixed)

    def check_if_antimeridian_crossing(self, lon_th=180) -> bool:
        """Check if the geometry across the antimeridian line,
           assuming the geometry's coordinates are in WGS84 Lon/Lat.

        Parameters
        ----------
        lon_th: int
            Max longitude range that the geometry falls in.

        Returns
        -------
        bool
            True if the geometry is antimeridian-crossing, otherwise False.
        """
        # antimeridian = LineString([(180, -90), (180, 0), (180, 90)])
        # return self._shapely_geom.intersects(antimeridian)
        def _is_polygon_cross_antimeridian(poly_coords):
            for ring in poly_coords:
                for i in range(len(ring) - 1):
                    # cross antimeridian if adjacent point lon > lon_th
                    lon0, lon1 = ring[i][0], ring[i + 1][0]
                    if (
                        lon0 == 180
                        or lon0 == -180
                        or lon1 == 180
                        or lon1 == -180
                        or abs(lon0 - lon1) > lon_th
                    ):
                        return True
            return False

        geom_json = self.to_geometry()
        g_type = geom_json["type"].upper()
        if g_type == "POLYGON":
            return _is_polygon_cross_antimeridian(geom_json["coordinates"])
        elif g_type == "MULTIPOLYGON":
            for poly_coords in geom_json["coordinates"]:
                if _is_polygon_cross_antimeridian(poly_coords):
                    return True
        else:
            logger.error(
                f"Unsupported geometry type of `{g_type}`, "
                "only support `Polygon` or `MultiPolygon`."
            )
            raise NotImplementedError
        return False

    def fix_antimeridian_crossing(self, lon_th=180) -> Geom:
        """Cut the antimeridian crossing geometry into list of geometries,
        with each of the small geometries not antimeridian crossing.

        Parameters
        ----------
        lon_th: int
            Max longitude range that the geometry falls in.

        Returns
        -------
        Geom
            Multiple small geometries that are not antimeridian-crossing.
        """
        if not self.check_if_antimeridian_crossing(lon_th=lon_th):
            logger.warning("This geometry doesn't cross Antimeridian, skip.")
            return self

        if self.geom_type != "Polygon":
            logger.warning("Only supports geometry type of `Polygon`.")
            raise NotImplementedError

        geom_json = json.loads(self.to_geometry_json())
        # negative longitude coordinates shift by +360 degrees
        for ring in geom_json["coordinates"]:
            for pt in ring:
                if pt[0] < 0:
                    pt[0] += 360

        # split new shifted geometry
        geom_shf = Geom(geom_json)
        geom_antimeridian = Geom.from_wkt("LINESTRING (180 -90, 180 90)")
        geom_splits = split(geom_shf.to_shapely(), geom_antimeridian.to_shapely())

        # shift split geometries back to original coordinates
        geom_fixed = Geom.create_an_empty_geom(geo_type="MultiPolygon")
        for g in geom_splits:
            geom_json = json.loads(Geom(g).to_geometry_json())
            g_coords = geom_json["coordinates"]
            if np.max(g_coords) > 180:
                for ring in g_coords:
                    for pt in ring:
                        pt[0] -= 360
            geom_fixed = geom_fixed.union(
                Geom({"type": geom_json["type"], "coordinates": g_coords})
            )
        return geom_fixed


def bbox_to_geom(bbox: BoundingBox) -> Geom:
    """Convert a Bounding Box to a polygon Geom.

    Parameters
    ----------
    bbox : BoundingBox
        Source bounding box to convert.

    Returns
    -------
    Geom
        A Polygon Geom representing the given bbox.
    """
    geometry = {
        "type": "Polygon",
        # counter-clock wise to follow standard
        "coordinates": [
            [
                [bbox.left, bbox.upper],
                [bbox.left, bbox.bottom],
                [bbox.right, bbox.bottom],
                [bbox.right, bbox.upper],
                [bbox.left, bbox.upper],
            ]
        ],
    }
    return Geom(geometry)
